package com.errol.sync.sync;

import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableLogic;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.toolkit.StringUtils;
import com.renjia.supervise.service.sync.pojo.EntityInfo;
import com.renjia.supervise.service.sync.pojo.SyncAdvisor;
import com.renjia.supervise.util.DateFormatUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

import java.lang.reflect.Field;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 数据同步的基础 service
 *
 * @author zhang xiao bin
 * @version v1.0
 * @date 2021/07/02
 * @since v
 **/
@Slf4j
@Service(value = "dataSyncService")
public class DataSyncServiceImpl implements DataSyncService {

    private final ConcurrentMap<Class<?>, EntityInfo<?>> entityCache;
    private final EntityInfo<?> empty;
    protected final com.renjia.supervise.service.sync.DatabaseSyncService databaseSyncService;

    public DataSyncServiceImpl(com.renjia.supervise.service.sync.DatabaseSyncService databaseSyncService) {
        this.databaseSyncService = databaseSyncService;
        this.entityCache = new ConcurrentHashMap<>();
        this.empty = new EntityInfo<>();
    }

    @Override
    @SuppressWarnings("unchecked")
    public final <E> EntityInfo<E> getEntityInfo(Class<E> entity) {
        if (!entityCache.containsKey(entity) || entityCache.get(entity) == empty) {
            entityCache.putIfAbsent(entity, empty);
            synchronized (entity) {
                if (entityCache.get(entity) == empty) {
                    try {
                        EntityInfo<E> entityInfo = new EntityInfo<>();
                        entityInfo.setEntityClass(entity);
                        TableName tableName = entity.getAnnotation(TableName.class);
                        if (tableName != null) {
                            entityInfo.setTableName(tableName.value());
                        }
                        SyncAdvisor tableSyncAdvisor = entity.getAnnotation(SyncAdvisor.class);
                        if (tableSyncAdvisor != null && StringUtils.isNotEmpty(tableSyncAdvisor.additionalWhereClause())) {
                            entityInfo.setAdditionalWhereClause(tableSyncAdvisor.additionalWhereClause());
                        }
                        List<EntityInfo.FieldInfo> fieldInfos = new ArrayList<>();
                        for (Field field : entity.getDeclaredFields()) {
                            if ("serialVersionUID".equals(field.getName())) {
                                continue;
                            }
                            EntityInfo.FieldInfo fieldInfo = new EntityInfo.FieldInfo();
                            fieldInfo.setPropertyName(field.getName());
                            TableField tableField = field.getAnnotation(TableField.class);
                            if (tableField != null && StringUtils.isNotEmpty(tableField.value())) {
                                fieldInfo.setFieldName(tableField.value());
                            } else {
                                fieldInfo.setFieldName(StringUtils.camelToUnderline(field.getName()));
                            }
                            String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
                            fieldInfo.setSetter(entity.getDeclaredMethod(setterName, field.getType()));
                            fieldInfos.add(fieldInfo);
                            TableLogic tableLogic = field.getAnnotation(TableLogic.class);
                            if (tableLogic != null) {
                                entityInfo.setLogicDeleteField(fieldInfo.getFieldName());
                            }
                            SyncAdvisor fieldSyncAdvisor = field.getAnnotation(SyncAdvisor.class);
                            if (fieldSyncAdvisor != null) {
                                if (StringUtils.isNotEmpty(fieldSyncAdvisor.fieldNameInSourceDatabase())) {
                                    fieldInfo.setFieldNameInSourceDatabase(fieldSyncAdvisor.fieldNameInSourceDatabase());
                                }
                                fieldInfo.setNotExistInSourceDatabase(fieldSyncAdvisor.notExistInSourceDatabase());
                            }
                        }
                        Class<?> superclass = entity.getSuperclass();
                        if (superclass != null && Object.class != superclass) {
                            fieldInfos.addAll(getEntityInfo(superclass).getFieldInfos());
                        }
                        entityInfo.setFieldInfos(fieldInfos);
                        entityCache.put(entity, entityInfo);
                    } catch (Exception e) {
                        log.error("同步实体类信息不完整", e);
                        return null;
                    }
                }
            }
        }
        return (EntityInfo<E>) entityCache.get(entity);
    }

    @Override
    public <E> String generateSelectClause(EntityInfo<E> entityInfo, boolean useFieldName) {
        StringBuilder sb = new StringBuilder();
        entityInfo.getFieldInfos().forEach(fieldInfo -> {
            if (!fieldInfo.isNotExistInSourceDatabase()) {
                if (sb.length() == 0) {
                    sb.append("select ");
                } else {
                    sb.append(",");
                }
                if (fieldInfo.getFieldNameInSourceDatabase() == null) {
                    sb.append("`").append(fieldInfo.getFieldName()).append("`");
                    if (!useFieldName && !fieldInfo.getFieldName().equals(fieldInfo.getPropertyName())) {
                        sb.append(" as ").append(fieldInfo.getPropertyName());
                    }
                } else {
                    sb.append("`").append(fieldInfo.getFieldNameInSourceDatabase()).append("`");
                    sb.append(" as ");
                    if (useFieldName) {
                        sb.append(fieldInfo.getFieldName());
                    } else {
                        sb.append(fieldInfo.getPropertyName());
                    }
                }
            }
        });
        sb.append(" from ").append(entityInfo.getTableName());
        return sb.toString();
    }

    @Override
    public <E> String generateWhereClause(EntityInfo<E> entityInfo, LocalDate rangeLeft, LocalDate rangeRight, boolean queryNewData) {
        String left = rangeLeft == null ? null : rangeLeft.toString();
        String right = rangeRight == null ? left : rangeRight.toString();
        StringBuilder sb = new StringBuilder();
        if (queryNewData) {
            if (entityInfo.getLogicDeleteField() != null) {
                sb.append(" where `status`=0");
            }
            if (rangeLeft != null) {
                if (entityInfo.getLogicDeleteField() != null) {
                    sb.append(" and ");
                } else {
                    sb.append(" where ");
                }
                sb.append(" create_time between '").append(left).append(" 00:00:00'").append(" and '").append(right).append(" 23:59:59'");
            }
            return sb.toString();
        } else if (rangeLeft != null) {
            sb.append(" where update_time between '").append(left).append(" 00:00:00'").append(" and '").append(right).append(" 23:59:59'");
            sb.append(" and create_time<'").append(left).append(" 00:00:00'");
            return sb.toString();
        }
        if (entityInfo.getAdditionalWhereClause() != null) {
            if (sb.length() == 0) {
                sb.append(" where ");
            } else {
                sb.append(" and ");
            }
            sb.append(entityInfo.getAdditionalWhereClause());
        }
        return sb.toString();
    }

    @Override
    public final <E> String syncData(Class<E> entityClass) {
        return syncData(entityClass, null, null, true);
    }

    @Override
    public final <E> String syncData(Class<E> entityClass, LocalDate syncDate, boolean updateEditedData) {
        return syncData(entityClass, syncDate, null, updateEditedData);
    }

    @Override
    public <E> String syncData(Class<E> entityClass, LocalDate rangeLeft, LocalDate rangeRight, boolean updateEditedData) {
        return doSyncWrapper(entityClass, rangeLeft, rangeRight, updateEditedData);
    }

    /**
     * 通过对 entityClass 加锁以避免并发同步单张数据表，但加锁失败会阻塞，挤占线程、影响性能
     * 该方法禁止重写，子类可以重写全参数的 syncData 方法，绕过该限制，但必须实现基础的并发安全
     * 子类通过重写 doSyncTable 方法实现特殊的同步逻辑，建议子类都通过 doSyncWrapper 触发表的同步
     *
     * @param <E>              实体类型
     * @param entityClass      实体类，加锁以控制并发
     * @param rangeLeft        日期区间左边界，包含
     * @param rangeRight       日期区间右边界，包含
     * @param updateEditedData true 在增量同步新增数据的同时，额外同步当天更新的数据
     * @return 提示信息，或 null 表示成功
     */
    protected final <E> String doSyncWrapper(Class<E> entityClass, @Nullable LocalDate rangeLeft, @Nullable LocalDate rangeRight, boolean updateEditedData) {
        String prefix = "";
        String errMsg = null;
        EntityInfo<E> entityInfo = null;
        try {
            synchronized (entityClass) {
                entityInfo = getEntityInfo(entityClass);
                entityInfo.getInSync().set(true);
                prefix = "数据表【" + entityInfo.getTableName() + "】";
                long start = System.currentTimeMillis();
                log.info(prefix + "开始同步，参数：rangeLeft=" + rangeLeft + ",rangeRight=" + rangeRight + ",updateEditedData=" + updateEditedData);

                this.doSyncTable(entityInfo, rangeLeft, rangeRight, updateEditedData);

                entityInfo.getInSync().set(false);
                log.info(prefix + "同步完成，用时：" + DateFormatUtil.stamp2String((System.currentTimeMillis() - start - 28800000), DateFormatUtil.PATTERN_F));
            }
        } catch (Exception e) {
            log.error(prefix + "同步异常", e);
            errMsg = e.getMessage();
            if (entityInfo != null) {
                entityInfo.getInSync().set(false);
            }
        }
        return errMsg;
    }

    /**
     * 对 entityInfo 描述的数据表进行同步
     * 有特殊同步逻辑的表重写该方法即可
     *
     * @param <E>              实体类型
     * @param entityInfo       表信息
     * @param rangeLeft        日期区间左边界，包含
     * @param rangeRight       日期区间右边界，包含
     * @param updateEditedData true 在增量同步新增数据的同时，额外同步当天更新的数据
     */
    protected <E> void doSyncTable(EntityInfo<E> entityInfo, @Nullable LocalDate rangeLeft, @Nullable LocalDate rangeRight, boolean updateEditedData) {
        String selectClause = generateSelectClause(entityInfo, true);
        // 查询增量数据，targetDate 为空，查询全部有效数据
        List<Map<String, Object>> dataToSave = databaseSyncService.loadData(selectClause, generateWhereClause(entityInfo, rangeLeft, rangeRight, true));
        this.onDataLoaded(dataToSave, true);
        if (rangeLeft == null) {
            // 全量更新
            databaseSyncService.fullSync(entityInfo, dataToSave, true);
        } else {
            // 增量更新
            List<String> idsToRemove = new ArrayList<>();
            if (updateEditedData) {
                // 编辑更新
                List<Map<String, Object>> updatedData = databaseSyncService.loadData(selectClause, generateWhereClause(entityInfo, rangeLeft, rangeRight, false));
                this.onDataLoaded(updatedData, false);
                updatedData.forEach(updated -> {
                    idsToRemove.add(updated.get("id").toString());
                    if (!updated.containsKey("status") || "0".equals(updated.get("status"))) {
                        dataToSave.add(updated);
                    }
                });
            }
            databaseSyncService.incrSync(entityInfo, dataToSave, true, rangeLeft, rangeRight, idsToRemove);
        }
    }

    /**
     * 从源数据库加载到数据后执行
     *
     * @param loadedData 查询到的数据
     * @param isNewData  true 表示源表新增数据，false 表示源表编辑更新的数据
     */
    protected void onDataLoaded(List<Map<String, Object>> loadedData, boolean isNewData) {
        // normally do nothing
    }
}
